/*
 * RevHttpNode 0.1 - Reverse HTTP proxy for NodeJS.
 *
 * Copyright 2010 Ivan Zuzak
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
 
 
/*
 * RevHttpNode is an simplified implementation ofthe Reverse HTTP specification
 * available at http://www.reversehttp.net/reverse-http-spec.html. The intended
 * use of the proxy is for exposing programs within browsers to rest of the WWW.
 *
 * Other projects used:
 *  - http://github.com/csausdev/log4js-node for logging
 *  - http://github.com/ncr/node.ws.js for WebSockets server
 *
 */

// TODO: edit
// logger for client and server
// browser client library
// WS implementation

/*

connection - {request, response, httpRequest, httpResponse, connectionId, logger}

// sort out data structures for queues - worker, processing, request

*/


var http = require('http');
var url = require('url');
var sys = require('sys');
var log4js = require('./lib/log4js-node');
var ws = require('./lib/ws');

var serviceDirectory = {};
var processingQueue = {};

function registerService(pathRegex) {
  if (typeof serviceDirectory[pathRegex] === "undefined") {
    serviceDirectory[pathRegex] = { workerQueue : [], requestQueue : [] };
  }
  return realPrefix;
}

function unregisterService(pathRegex) {
  if (typeof serviceDirectory[pathRegex] !== "undefined") {
    
    for (var i=0; i<serviceDirectory[pathRegex].workerQueue.length; i++) {
      var connection = serviceDirectory[pathRegex].workerQueue[i];
      connection.response.writeHead(404, "Not Found");
      connection.response.close();
    }
    
    for (var i=0; i<serviceDirectory[pathRegex].requestQueue.length; i++) {
      var connection = serviceDirectory[pathRegex].requestQueue[i];
      connection.response.writeHead(404, "Not Found");
      connection.response.close();
    }
        
    delete serviceDirectory[pathRegex];
  }
}

function sendServerRequestResponse(serverConnection, clientConnection, pathRegex) {
  var requestID = clientConnection.request.requestId;
  processingQueue[requestID] = {requestID : requestID, request : clientConnection.request, response : clientConnection.response};
  var curHttpResponse = serverConnection.response.curHttpResponse;
  curHttpResponse.statusCode = 200;
  curHttpResponse.reasonPhrase = "OK";
  curHttpResponse.headers = { "Content-Type" : "application/http", "x-revhttp" : escape(JSON.stringify( {Method : "REQUEST-RESPONSE", pathRegex : pathRegex, RequestID : requestID})), "access-control-allow-origin" : '*'};
  curHttpResponse.body = JSON.stringify(clientConnection.request.curHttpRequest);
  
  serverConnection.response.writeHead(curHttpResponse.statusCode, curHttpResponse.reasonPhrase, curHttpResponse.headers);
  serverConnection.response.write(curHttpResponse.body);
  serverConnection.response.close();
}

function processClientRequest(request, response) {

  var logger = log4js.getLogger('mw-client req ' + getNextReqNum().toString());
  logger.setLevel(globalDebugLevel);
  request.connection.logger = logger;
  
  request.requestId = generateUUID();
  request.connection.addListener("end", function() {
    var requestId = request.requestId;
    logger.debug("Client connection closed.");
    
    if (typeof request.pathRegex !== "undefined" && request.pathRegex !== null) {
      sys.puts(request.pathRegex);
      for (var i=0; i<serviceDirectory[pathRegex].requestQueue.length; i++) {
        if (serviceDirectory[pathRegex].requestQueue[i].request.requestId === requestId) {
          logger.debug("Removing client connection from request queue.");
          serviceDirectory[pathRegex].requestQueue.splice(i, 1);
          break;
        }
      }

      if (typeof processingQueue[requestId] !== "undefined") {
        logger.debug("Removing client connection from processing queue.");
        delete processingQueue[requestId];
      }
    }
  });  
  
  var curHttpRequest = {method : request.method, url : request.url, httpVersion : "HTTP/" + request.httpVersion, headers : request.headers};
  request.curHttpRequest = curHttpRequest;
  var curHttpResponse = {statusCode : null, reasonPhrase : null, httpVersion : "HTTP/1.1", headers : {"access-control-allow-origin" : '*'} };
  response.curHttpResponse = curHttpResponse;
  
  logger.info("Received new request: " + request.method + " " + request.url);
  var requestUri = url.parse(request.url).pathname;
  var pathRegex = getMatchingRegex(requestUri);
  request.pathRegex = pathRegex;
  
  if (pathRegex === null) {
    logger.error("No registered server matches requested URI.");
    curHttpResponse.statusCode = 404;
    curHttpResponse.reasonPhrase = "Not Found";
    response.writeHead(curHttpResponse.statusCode, curHttpResponse.reasonPhrase, curHttpResponse.headers);
    response.close();
  } else {
    logger.debug("Found matching server.");
    request.addListener("data", function (chunk) {
      logger.debug("Received data chunk.");
      curHttpRequest.body = typeof curHttpRequest.body === "undefined" ? chunk : curHttpRequest.body + chunk;
    });
    request.addListener("end", function() {
      logger.debug("Received full request.");
      logger.trace("Full request: " + JSON.stringify(curHttpRequest));
      
      var serverConnection = serviceDirectory[pathRegex].workerQueue.shift();
      if (typeof serverConnection === "undefined") {
        serviceDirectory[pathRegex].requestQueue.push( {request : request, response : response} );
        logger.debug("No server connection found, request queued.");
      } else {
        logger.debug("Found server connection for client request.");
        sendServerRequestResponse(serverConnection, {request : request, response : response}, pathRegex);
        logger.debug("Sent client request to server.")
        serverConnection.request.connection.logger.debug("Sent client request to server as response."); // response sent to server: " + JSON.stringify(serverConnection.response.curHttpResponse));
      }
    });
  }
}

// helpers

var reqNumber = 0;
function getNextReqNum() {
  reqNumber+=1;
  return reqNumber;
}
var globalDebugLevel = 'DEBUG';
function generateUUID() {
    var uuid = [], nineteen = "89AB", hex = "0123456789ABCDEF";
    for (var i=0; i<36; i++) {
      uuid[i] = hex[Math.floor(Math.random() * 16)];
    }
    uuid[14] = '4';
    uuid[19] = nineteen[Math.floor(Math.random() * 4)];
    uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
    return uuid.join('');
}

function getMatchingRegex(requestUri) {
  for (var regex in serviceDirectory) {
    if (requestUri.match(regex)) {
      return regex
    }
  }
  
  return null;
}

function processHttpServerRequest(connection) {

  request.connection.addListener("end", function() {
    var requestId = request.requestId;
    logger.debug("Server connection closed.");
    
    if (typeof request.pathRegex !== "undefined" && request.pathRegex !== null) {
      for (var i=0; i<serviceDirectory[pathRegex].workerQueue.length; i++) {
        if (serviceDirectory[pathRegex].workerQueue[i].request.requestId === requestId) {
          logger.debug("Removing server connection from worker queue.");
          serviceDirectory[pathRegex].workerQueue.splice(i, 1);
          break;
        }
      }
    }
  });
  
  var body = "";
  connection.request.addListener("data", function (chunk) { 
    connection.logger.debug("Received data chunk.");
    body += chunk;
  });
  
  connection.request.addListener("end", function() {
    connection.logger.debug("Received full request.");
    // print full request connection.logger.trace();
    processServerRequest(connection.curHttpRequest.body);
  });
}

// register (pathRegex) returns success realPrefix registrationId
// addConnection (pathRegex, connection) 
// unregister (pathregex, id) returns success
// removeConnection (connection) 
// getconnection (pathRegex) returns connection (may remove it from queue, may not)
function processServerRequest(requestString, connection) {
  var request = JSON.parse(data);

  if (typeof request["revhttp"] === "undefined" || request["revhttp"] === null) {
    connection.logger.debug("No revhttp header found, closing connection.");
    connection.close();
  
  } else {
    var pathRegex = request["revhttp"].pathRegex;
    
    if (request["x-revhttp"].Method === "REGISTER") {
      connection.logger.debug("Processing REGISTER request for pathRegex " + pathRegex);
      var regResponse = registerService(pathRegex);
      if (connection.connectionType === "ws") {
        addWorker(pathRegex, connection);
      }
      var response = JSON.stringify({"x-revhttp" : {Method : "REGISTER-RESPONSE", pathRegex : pathRegex, realPrefix : realPrefix, response : regResponse}});
      connection.sendMessage(response);
  
    } else if (request["revhttp"].Method === "UNREGISTER") {
      connection.logger.debug("Processing UNREGISTER request for pathRegex " + pathRegex);
      var unregResponse = unregisterService(pathRegex, "fakeRegistrationId");
      var response = JSON.stringify({"x-revhttp" : {Method : "UNREGISTER-RESPONSE", pathRegex : pathRegex, response : unregResponse}});
      connection.sendMessage(response);
  
    } else if (request["revhttp"].Method === "RESPONSE" ) {
      connection.logger.debug("Processing RESPONSE request for pathRegex " + pathRegex);
      var requestId = request["revhttp"].requestId;
      
      var clientConnection = getClientConnection(requestId);
      connection.logger.debug("Found client request for server response.");
      
      clientConnection.sendMessage(request.http);      
      connection.logger.debug("Sent response to client.");
      clientRequest.request.connection.logger.debug("Sent client response: " + JSON.stringify(request.http));
      if (connection.connectionType === "http") {
        addWorker(pathRegex, connection);
      }
      
    } else if (request["revhttp"].Method === "REQUEST" ) {
      connection.logger.debug("Processing REQUEST request for pathRegex " + pathRegex);
      if (connection.connectionType === "http") {
        addWorker(pathRegex, connection);
      }

    } else {
      connection.logger.debug("Unknown revhttp method " + request["revhttp"].Method + "found, closing server connection.");
      connection.close();
    }
  }
}

function processWebSocket(connection) {  
  logger.debug("New server attempting to connect to websocket.");
  
  connection.websocket.addListener("connect", function (resource) {
    connection.logger.debug("New server connected to websocket resource " + resource);
  });
  
  connection.websocket.addListener("data", function (data) {
    processServerMessage(serverMessage, connection);
  });

  connection.websocket.addListener("close", function () {
    // unregister everything connected to this socket
    // remove all workers for this socket
    connection.logger.debug("WebSocket closed.");
  });
}

log4js.addAppender(log4js.consoleAppender());

function createRevHttpProxy() {
  var revHttp = {};
  
  revHttp.logger = log4js.getLogger('RevHttpProxy');
  revHttp.logger.setLevel('DEBUG');
  
  setLogLevel = function(logLevel) {
    revHttp.logger.setLevel(logLevel);
  }
  
  listen = function(clientSideHttpPort, clientSideHttpHost, serverSideHttpPort, serverSideHttpHost, serverSideWSPort, serverSideWSHost) {
  
    function setupConnection(connection) {
      connection.connectionId = generateUUID();
      
      var loggerPrefix = connection.connectionType + "-" + connection.side + "-" + getNextReqNum().toString();
      connection.logger = log4js.getLogger(loggerPrefix);
      connection.logger.setLevel('DEBUG');
      
      if (connection.connectionType === "http") {
        connection.request.connection.setTimeout(0);
      }
      
      if (connection.connectionType === "http" && connection.side === "client") {
        connection.sendMessage = function (message) {
          connection.response.writeHead(message.statusCode, message.reasonPhrase, message.headers);
          if (typeof message.body !== "undefined" && message.body !== null) {
            connection.response.write(message.body);
          }
          connection.response.close();
        };
      } else if (connection.connectionType === "http" && connection.side === "server") {
        connection.sendMessage = function (message) {
          var msg = JSON.stringify(message);
          connection.response.writeHead(200, "OK", {"content-length" : msg.length, "origin" : "access-control-allow-origin" : '*'} );
          connection.response.write(msg);
          connection.response.close();
        };
      } else if (connection.connectionType === "ws" && connection.side === "server") {
        connection.sendMessage = function (message) {
          connection.websocket.write(message);
        }
      }
    }
  
    revHttp.clientHttpServer = http.createServer(function(request, response) {
      var connection = { connectionType : "http", side : "client", request : request, response : response };
      setupConnection(connection);
      processHttpClientRequest(connection);
    });
    
    revHttp.serverHttpServer = http.createServer(function(request, response) {
      var connection = { connectionType : "http", side : "server", request : request, response : response };
      setupConnection(connection);
      processHttpServerRequest(connection);
    });
    
    revHttp.serverWSServer = ws.createServer(function(websocket) {
      var connection = { connectionType : "ws", side : "server", websocket : websocket };
      setupConnection(connection);
      processWebSocket(connection);
    });
    
    revHttp.clientHttpServer.listen(clientSideHttpPort, clientSideHttpHost);
    revHttp.serverHttpServer.listen(serverSideHttpPort, serverSideHttpHost);
    revHttp.serverWSServer.listen(serverSideWSPort, serverSideWSHost);

    revHttp.logger.warn('Proxy started: client-side on ' + clientSideHttpHost + ":" + clientSideHttpPort.toString() + ' server-side HTTP on ' + serverSideHttpHost + ":" + serverSideHttpPort.toString() + ' server side WebSocket on ' + serverSideWSHost + ":" + serverSideWSPort.toString());
  };
  
  close = function() {
    proxyLogger.warn('Proxy stopped.');
    
    revHttp.clientHttpServer.close();
    revHttp.serverHttpServer.close();
    revHttp.serverWSServer.close();
  };
  
  return {
    listen : listen,
    close : close,
    setLogLevel : setLogLevel
  };
}

var realPrefix = 'http://tirion.zemris.fer.hr:9899';

var mwServerWSHost = '192.168.8.129', 
    mwServerWSPort = 9897, 
    mwServerHTTPHost = '192.168.8.129', 
    mwServerHTTPPort = 9898,
    mwClientHTTPHost = '192.168.8.129', 
    mwClientHTTPPort = 9899;

createRevHttpProxy().listen(mwClientHTTPPort, mwClientHTTPHost, mwServerHTTPPort, mwServerHTTPHost, mwServerWSPort, mwServerWSHost);
